var $$ = $$ || {};
$$.Animation = $$.Animation || {};

/**
 * The simulator should be accessed via $$.Animation.MultiSpringElement.
 * @constructor
 */
$$.Animation.MultiSpring = function() {
	this.frozen = [];

	this.mass = [];
	this.pos = [];
	this.velocity = [];
	
	this.targetPos = [];
	this.maxVelocity = [];
	this.rigidness = [];
	this.springForceGamma = []; // Spring force is raised into this power.
	this.damping = [];
	this.barrierDamping = [];
	this.barrierStretch = [];
	
	this.targetMaxVelocity = [];
	this.targetVelocityLimitGamma = [];
	this.targetEdgeStopEnabled = [];
	
	this.positionLimits = [];
	
	this.callbacks = [];

	this.fps = 0;

	/**
	 * @return {Number} Index of the added element.
	 */
	this.addElement = function() {
		this.frozen.push(false);
		this.mass.push(1);
		this.pos.push(0);
		this.velocity.push(0);
		this.targetPos.push(0);
		this.maxVelocity.push(null);
		this.rigidness.push(1);
		this.springForceGamma.push(1);
		this.damping.push(0);
		this.barrierDamping.push(null);
		this.barrierStretch.push(10);
		this.targetMaxVelocity.push(null);
		this.targetVelocityLimitGamma.push(1.5);
		this.targetEdgeStopEnabled.push(false);
		this.positionLimits.push(null);
		this.callbacks.push(0);

		return this.mass.length - 1;
	};
	
//	this.setPosAndStop = function(pos) {
//		this.pos = pos;
//		this.targetPos = pos;
//
//		var v = [];
//
//		for (var i = 0; i < pos.length; i++)
//			v.push(0);
//
//		this.velocity = v;
//	};

	this.getZeta = function(i) {
		return this.damping[i] / (2 * Math.sqrt(this.mass[i] * this.rigidness[i]));
	};
	
	/**
	 * Sets damping via "zeta", see http://en.wikipedia.org/wiki/Damping.
	 * Mass and rigidness should already be set.
	 */
	this.setZeta = function(i, zeta) {
		this.damping[i] = 2 * Math.sqrt(this.mass[i] * this.rigidness[i]) * zeta;
	};
	
	this._lastTime = +(new Date());
	
	setInterval(_.bind(function() {
		var now = +(new Date());
		var time = (now - this._lastTime) / 1000;
		var dt = 0.01;

		// Slow down if we're doomed.
		if (time > 0.2)
			time = 0.2;
		
		var i, ni = this.pos.length;
		var dx, dx2, damping, dir, force, acc, targetMaxVelocity, maxVelocity, positionLimits;
		
		while (time > 0.000001) {
			for (i = 0; i < ni; i++) {
				if (this.frozen[i])
					continue;

				dx = this.targetPos[i] - this.pos[i];
				dir = dx > 0 ? 1 : -1;
				
				damping = this.damping[i];

				if(this.barrierDamping[i] !== null)
					damping += this.barrierDamping[i] * $$.Math.erfca(dx / this.barrierStretch[i]);

				force = dir * Math.pow(Math.abs(dx), this.springForceGamma[i]) * this.rigidness[i]
					- this.velocity[i] * damping;
				acc = force / this.mass[i];
				
				this.velocity[i] += acc * dt;

				maxVelocity = this.maxVelocity[i];
				targetMaxVelocity = this.targetMaxVelocity[i];
				
				if (targetMaxVelocity !== null) {
					targetMaxVelocity += Math.pow(Math.abs(dx), this.targetVelocityLimitGamma[i]);

					if (maxVelocity === null || targetMaxVelocity < maxVelocity)
						maxVelocity = targetMaxVelocity;
				}
				
				if (maxVelocity !== null && Math.abs(this.velocity[i]) > maxVelocity) {
					if (this.velocity[i] >= 0)
						this.velocity[i] = maxVelocity;
					else
						this.velocity[i] = -maxVelocity;
				}
				
				this.pos[i] += this.velocity[i] * dt;
				
				if (this.targetEdgeStopEnabled[i]) {
					dx2 = this.targetPos[i] - this.pos[i];
					
					if (dx > 0 && dx2 < 0 || dx < 0 && dx2 > 0) {
						this.pos[i] = this.targetPos[i];
						this.velocity[i] = 0;
						continue; // Next `i`.
					}
				}
				
				if (this.positionLimits[i] !== null) {
					positionLimits = this.positionLimits[i];

					if (this.pos[i] < positionLimits[0]) {
						this.pos[i] = positionLimits[0];
						this.velocity[i] = 0;
					} else if (this.pos[i] > positionLimits[1]) {
						this.pos[i] = positionLimits[1];
						this.velocity[i] = 0;
					}
				}
			} // for (i)
			
			time -= dt;
		} // while

		this.fps = 1000 / (now - this._lastTime);
		this._lastTime = now;

		for (var i = 0, ni = this.callbacks.length; i < ni; i++) {
			if (this.callbacks[i])
				this.callbacks[i].call();
		}
	}, this), 20);
};

// We'll have a single multispring simulator.

$$.Animation.multiSpring = new $$.Animation.MultiSpring();

/**
 * Accessor is bound to a new element withing simulator when created.
 *
 * @example
 *
 * var spring = new $$.Animation.MultiSpringElement();
 * spring.setCallback(function() { ... });
 * ...
 *
 * @constructor
 */
$$.Animation.MultiSpringElement = function() {
	this._index = $$.Animation.multiSpring.addElement();
};

// DELICIOUS COPY-PASTE, MMM...

$$.Animation.MultiSpringElement.prototype.freeze = function(flag) {
	$$.Animation.multiSpring.frozen[this._index] = !!flag;
};

$$.Animation.MultiSpringElement.prototype.mass = function(mass) {
	if (typeof mass === 'undefined')
		return $$.Animation.multiSpring.mass[this._index];

	$$.Animation.multiSpring.mass[this._index] = mass;
};

$$.Animation.MultiSpringElement.prototype.pos = function(pos) {
	if (typeof pos === 'undefined')
		return $$.Animation.multiSpring.pos[this._index];

	$$.Animation.multiSpring.pos[this._index] = pos;
};

$$.Animation.MultiSpringElement.prototype.velocity = function(velocity) {
	if (typeof velocity === 'undefined')
		return $$.Animation.multiSpring.velocity[this._index];

	$$.Animation.multiSpring.velocity[this._index] = velocity;
};

$$.Animation.MultiSpringElement.prototype.targetPos = function(targetPos) {
	if (typeof targetPos === 'undefined')
		return $$.Animation.multiSpring.targetPos[this._index];

	$$.Animation.multiSpring.targetPos[this._index] = targetPos;
};

$$.Animation.MultiSpringElement.prototype.maxVelocity = function(maxVelocity) {
	if (typeof maxVelocity === 'undefined')
		return $$.Animation.multiSpring.maxVelocity[this._index];

	$$.Animation.multiSpring.maxVelocity[this._index] = maxVelocity;
};

$$.Animation.MultiSpringElement.prototype.rigidness = function(rigidness) {
	if (typeof rigidness === 'undefined')
		return $$.Animation.multiSpring.rigidness[this._index];

	$$.Animation.multiSpring.rigidness[this._index] = rigidness;
};

$$.Animation.MultiSpringElement.prototype.springForceGamma = function(springForceGamma) {
	if (typeof springForceGamma === 'undefined')
		return $$.Animation.multiSpring.springForceGamma[this._index];

	$$.Animation.multiSpring.springForceGamma[this._index] = springForceGamma;
};

$$.Animation.MultiSpringElement.prototype.damping = function(damping) {
	if (typeof damping === 'undefined')
		return $$.Animation.multiSpring.damping[this._index];

	$$.Animation.multiSpring.damping[this._index] = damping;
};

$$.Animation.MultiSpringElement.prototype.zeta = function(zeta) {
	if (typeof zeta === 'undefined')
		return $$.Animation.multiSpring.getZeta(this._index);

	$$.Animation.multiSpring.setZeta(this._index, zeta);
};

$$.Animation.MultiSpringElement.prototype.barrierDamping = function(barrierDamping) {
	if (typeof barrierDamping === 'undefined')
		return $$.Animation.multiSpring.barrierDamping[this._index];

	$$.Animation.multiSpring.barrierDamping[this._index] = barrierDamping;
};

$$.Animation.MultiSpringElement.prototype.barrierStretch = function(barrierStretch) {
	if (typeof barrierStretch === 'undefined')
		return $$.Animation.multiSpring.barrierStretch[this._index];

	$$.Animation.multiSpring.barrierStretch[this._index] = barrierStretch;
};

$$.Animation.MultiSpringElement.prototype.targetMaxVelocity = function(targetMaxVelocity) {
	if (typeof targetMaxVelocity === 'undefined')
		return $$.Animation.multiSpring.targetMaxVelocity[this._index];

	$$.Animation.multiSpring.targetMaxVelocity[this._index] = targetMaxVelocity;
};

$$.Animation.MultiSpringElement.prototype.targetVelocityLimitGamma = function(targetVelocityLimitGamma) {
	if (typeof targetVelocityLimitGamma === 'undefined')
		return $$.Animation.multiSpring.targetVelocityLimitGamma[this._index];

	$$.Animation.multiSpring.targetVelocityLimitGamma[this._index] = targetVelocityLimitGamma;
};

$$.Animation.MultiSpringElement.prototype.targetEdgeStopEnabled = function(targetEdgeStopEnabled) {
	if (typeof targetEdgeStopEnabled === 'undefined')
		return $$.Animation.multiSpring.targetEdgeStopEnabled[this._index];

	$$.Animation.multiSpring.targetEdgeStopEnabled[this._index] = targetEdgeStopEnabled;
};

$$.Animation.MultiSpringElement.prototype.positionLimits = function(positionLimits) {
	if (typeof positionLimits === 'undefined')
		return $$.Animation.multiSpring.positionLimits[this._index];

	$$.Animation.multiSpring.positionLimits[this._index] = positionLimits;
};

$$.Animation.MultiSpringElement.prototype.callback = function(callback, context) {
	if (arguments.length == 0)
		return $$.Animation.multiSpring.callbacks[this._index];

	if (typeof context !== 'undefined')
		callback = _.bind(callback, context);

	$$.Animation.multiSpring.callbacks[this._index] = callback;
};
